-- Banfail by Stouie
-- $Id: Banfail.lua 6 2009-05-18 08:16:08Z stoupik $

----------------------
-- Revision history --
----------------------

-- Version 0.2:
 -- Simple display
 -- Autocheck

-- Version 0.1:
 -- Initial version
 -- Manual checking
 -- Console output
 -- Hardcoded configs

Banfail = {
	revision = ("$Revision: 6 $"):sub( 12, -3 ),
	version = "0.2",
	display_version = "0.2"
}

BF_SavedOptions = {
}

Banfail.default_options = {
    debug = false,
    autocheck = true,
    print_checks = true,
    show_display = true,
}

--
-- Locals
--

local main_frame = CreateFrame( "Frame" )

--
-- Event handling
--

do
	local registered_events = {}

	local function handle_event( self, event, ... )
        --if Banfail.print then
        --    Banfail:print( ( "Event %s fired" ):format( event ) )
        --end
        
		if not registered_events[ event ] then
            return
        end

        for i, v in ipairs( registered_events[ event ] ) do
			if type( v[ event ] ) == "function" then
				v[ event ]( v, ... )
			end
		end
	end

	function Banfail:register_events( ... )
		for i = 1, select( "#", ... ) do
			local event = select( i, ... )
			registered_events[ event ] = registered_events[ event ] or {}
			tinsert( registered_events[ event ], self )
			--if Banfail.print then
            --    Banfail:print( ( "Registered event %s" ):format( tostring( event ) ) )
            --end
			main_frame:RegisterEvent( event )
		end
	end

	function Banfail:unregister_all_events()
		for i, v in pairs( registered_events ) do
			for i = #v, 1 do
				if v[ i ] == self then
					tremove( v, i )
				end
			end
			if #v == 0 then
				registered_events[ i ] = nil
				main_frame:UnregisterEvent( i )
			end
		end
	end

	Banfail:register_events( "ADDON_LOADED" )
	main_frame:SetScript( "OnEvent", handle_event )
end

--
-- Timer scheduling
--

-- FIXME: TODO

--
-- Command-line
--

SLASH_BANFAIL1 = "/banfail"
SLASH_BANFAIL2 = "/bf"

SlashCmdList[ "BANFAIL" ] = function( msg )
	local cmd = msg:lower()
	if cmd == "ver" or cmd == "version" then
		Banfail:print_version()
	elseif cmd == "debug" then
        Banfail:print_options_debug()
    elseif cmd == "debug on" then
        Banfail.options.debug = true
        Banfail:print_options_debug()
    elseif cmd == "debug off" then
        Banfail.options.debug = false
        Banfail:print_options_debug()
    elseif cmd:sub( 0, 4 ) == "role" then
        local role = msg:match( "^%w+%s+(.-)%s*$" )
        if role then
            Banfail:set_role( role )
        else
            Banfail:print_role()
        end
    elseif cmd == "check" then
        Banfail:check_role()
    elseif cmd == "autocheck" then
        Banfail:print_options_autocheck()
    elseif cmd == "autocheck on" then
        Banfail:enable_autocheck()
        Banfail:print_options_autocheck()
    elseif cmd == "autocheck off" then
        Banfail:disable_autocheck()
        Banfail:print_options_autocheck()
    elseif cmd == "show" then
        Banfail:show_display()
    elseif cmd == "hide" then
        Banfail:hide_display()
    elseif cmd == "resetpos" then
        Banfail:reset_display()
        Banfail:show_display()
    elseif cmd:sub( 0, 4 ) == "link" then
        local link = Banfail:format_link( msg:sub( 5 ) )
        Banfail:print( link )
    elseif cmd == "help" then
        Banfail:print_help()
    else
        Banfail:print_warning( ( "Unknown command %s" ):format( msg ) )
        Banfail:print_help()
    end
end

--
-- Messages
--

function Banfail:print( text, prefix )
	prefix = prefix or "Banfail"
	DEFAULT_CHAT_FRAME:AddMessage( ( "|cffff7d0a<|r|cffffd200%s|r|cffff7d0a>|r %s" ):format( tostring( prefix ), tostring( text ) ) )
end

function Banfail:print_debug( text )
    if self.options and self.options.debug then
        self:print( text, "Banfail debug" )
    end
end

function Banfail:print_warning( text, prefix )
    Banfail:print( ( "|cffff4040%s|r" ):format( tostring( text ) ), prefix )
end

function Banfail:print_version()
    local revision = tonumber( self.revision ) or 0
    local v = ( "%s (r%d)" ):format( self.display_version, revision )
    self:print( v )
end

function Banfail:print_options_debug()
    local debug = self.options and self.options.debug
    -- Banfail:print( ( "Debug %s" ):format( debug ? "on" : "off" ) )
    if debug then
        Banfail:print( "Debug on" )
    else
        Banfail:print( "Debug off" )
    end
end

function Banfail:print_options_autocheck()
    local autocheck = self.options and self.options.autocheck
    if autocheck then
        Banfail:print( "Autocheck on" )
    else
        Banfail:print( "Autocheck off" )
    end
end

function Banfail:print_role()
    local role = self.options and self.options.role
    if role then
        Banfail:print( ( "Current role is %s" ):format( tostring( role ) ) )
    else
        Banfail:print( "No role currently set" )
    end
end

function Banfail:print_help()
    Banfail:print( "Available Banfail commands:" )
    Banfail:print( "/bf role ROLE - set current role to ROLE" )
    Banfail:print( "/bf check - check conditions for current role" )
    Banfail:print( "/bf autocheck [on||off] - enable/disable automatic checking" )
    Banfail:print( "/bf show||hide||resetpos - show/hide/reset fail display" )
    Banfail:print( "/bf link LINK - print shift-click link in the nasty format")
    Banfail:print( "/bf ver||version - print Banfail version" )
    Banfail:print( "/bf debug [on||off] - turn debugging output on/off" )
    Banfail:print( "/bf help - print command-line help" )
end

-- Probably something like this in lua already, like perl's data::dump?
function Banfail:format_table( table, bracketize )
    if type( table ) ~= "table" then
        return tostring( table )
    end
    
    local result = ""
    if bracketize then
        result = "{ "
    end
    local print_comma = false
    for i, v in pairs( table ) do
        if print_comma then
            result = result .. ", "
        else
            print_comma = true
        end
        
        result = result .. ( "%s = %s" ):format( tostring( i ), self:format_table( v, bracketize ) )
    end
    
    if bracketize then
        result = result .. " }"
    end
    return result
end

-- Probably something like this in lua already, like perl's data::dump?
function Banfail:format_array( table, bracketize )
    if type( table ) ~= "table" then
        return tostring( table )
    end

    local result = ""
    if bracketize then
        result = "{ "
    end
    local print_comma = false
    for i, v in pairs( table ) do
        if print_comma then
            result = result .. ", "
        else
            print_comma = true
        end

        result = result .. ( "%s" ):format( self:format_table( v, bracketize ) )
    end

    if bracketize then
        result = result .. " }"
    end
    return result
end

-- Mangle item/spell/whatever link so the string is actually printed, instead of clickable link
function Banfail:format_link( link )
    return gsub( link, "\124", "\124\124" )
end

--
-- Initialization
--

do
    function Banfail:copy_defaults( to, from )
		for i, v in pairs( from ) do
			if to[ i ] == nil then
				to[ i ] = v
			elseif type( v ) == "table" then
				return copy_defaults( to[ i ], v )
			end
		end
    end

    function Banfail:load_options()
		self.options = BF_SavedOptions
		self:copy_defaults( self.options, self.default_options )
    end

    function Banfail:load_checks()
        self.checks = {}
        local character_name = UnitName( "player" )
        local checks_name = "BF_DefaultChecks_" .. character_name
        local default_checks = _G[ checks_name ]
        if default_checks and type( default_checks ) == "table" then
            self.checks = default_checks
        else
            self:print_warning( ( "Can't find configuration for character %s" ):format( character_name ) )
        end
    end

	function Banfail:ADDON_LOADED( modname )
		if modname ~= "Banfail" then
            return
        end

        self:load_options()
        self:print_debug( "Initializing" )

        self:create_display()

        self:load_checks()
        self.fail_list = {}
        if self.options.role then
            self:set_role( self.options.role )
        end
        
        if self.options.autocheck then
            self:enable_autocheck()
        else
            self:disable_autocheck()
        end
	end
end

--
-- Display
--

-- FIXME: change to ScrollingMessageFrame?

function Banfail:create_display()
    self.display = CreateFrame( "SimpleHTML", "BanfailDisplay", UIParent )
    self.display:SetFrameStrata( "MEDIUM" )
    self.display:SetFont( "Fonts\\FRIZQT__.TTF", 11 )
    self.display:SetBackdrop( {
        bgFile = "Interface/Tooltips/UI-Tooltip-Background", tile = true
    } )
    self.display:SetBackdropColor( 0, 0, 0, 0.25 )
    self.display:SetWidth( 120 )
    self.display:SetHeight( 24 )
    self.display:SetPoint( "CENTER", 0, 0 )
    self.display:SetMovable( true )
    self.display:EnableMouse( true )
    self.display:SetClampedToScreen( true )
    self.display:SetScript( "OnMouseDown", function( self ) self:StartMoving() end )
    self.display:SetScript( "OnMouseUp", function( self ) self:StopMovingOrSizing() end )
    self.display:SetScript( "OnDragStop", function( self ) self:StopMovingOrSizing() end )
    --self.display:SetText( "<html><body><h1>SimpleHTML Demo: Ambush</h1><img src=\"Interface\Icons\Ability_Ambush\" width=\"32\" height=\"32\" align=\"right\"/><p align=\"center\">|cffee4400'You think this hurts? Just wait.'|r</p><br/><br/><p>Among every ability a rogue has at his disposal,<br/>Ambush is without a doubt the hardest hitting Rogue ability.</p></body></html>" )
    --self.display:SetText( "<html><body><p>You are ROLE</p><p>And you fail at life</p></body></html>" )
    self.display:SetText( "<html><body><p></p></body></html>" )

    if self.options.show_display then
        self:show_display()
    else
        self:hide_display()
    end
end

function Banfail:show_display()
    self.options.show_display = true
    self.display:Show()
end

function Banfail:hide_display()
    self.options.show_display = false
    self.display:Hide()
end

function Banfail:reset_display()
    self.display:ClearAllPoints()
    self.display:SetPoint( "CENTER", 0, 0 )
end

function Banfail:update_display()
    local header = ( "I am %s" ):format( self.options.role )
    local list = self.fail_list

    if #list == 0 then
        self.display:SetHeight( 24 )
        self.display:SetText( ( "<html><body><p>%s</p><p>And I am awesome</p></body></html>" ):format( header ) )
        return
    end
    
    local fails = ""
    for i, v in ipairs( list ) do
        fails = fails .. ( "<p>- %s</p>" ):format( tostring( v ) )
    end
    self.display:SetHeight( 24 + 12 * #list ) -- FIXME: how to make it grow down, not both sides? How to compute the size properly?
    self.display:SetText( ( "<html><body><p>%s</p><p>|cffff4040And I fail at:|r</p>%s</body></html>" ):format( header, fails ) )
end

--
-- Autocheck
--

function Banfail:enable_autocheck()
    self.options.autocheck = true
    self:register_events(
        "ZONE_CHANGED_NEW_AREA",
        "PLAYER_TALENT_UPDATE",
        "UNIT_INVENTORY_CHANGED",
        "UPDATE_SHAPESHIFT_FORM"
        -- FIXME: glyph change?
    )
end

function Banfail:disable_autocheck()
    self.options.autocheck = false
    self:unregister_all_events()
    self:register_events( "ADDON_LOADED" )
end

function Banfail:ZONE_CHANGED_NEW_AREA()
    self:autocheck_role()
end

function Banfail:PLAYER_TALENT_UPDATE()
    self:autocheck_role()
end

function Banfail:UNIT_INVENTORY_CHANGED( unit )
    if unit ~= "player" then
        return
    end
    self:autocheck_role()
end

function Banfail:UPDATE_SHAPESHIFT_FORM()
    self:autocheck_role()
end

--
-- Role checking
--

function Banfail:set_role( role )
    if not self.checks[ role ] then
        self:print_warning( ( "%s is not a valid role, try one of these instead:" ):format( role ) )
        for i, v in pairs( self.checks ) do
            self:print( ( "   %s" ):format( i ) )
        end
        return
    end

    self.options.role = role
    self.role_checks = self.checks[ role ]
    self:print( ( "Role set to %s" ):format( role ) )
    self:autocheck_role()
end

function Banfail:autocheck_role()
    local print_checks = self.options.print_checks
    self.options.print_checks = false
    self:check_role()
    self.options.print_checks = print_checks
end

function Banfail:check_role()
    local checks = self.role_checks
    if not checks then
        self:print_warning( "No valid role set - you fail in a way" )
        return
    end
    
    -- Time to perform the checks
    
    if self.options.print_checks then
        self:print( ( "Checking role %s" ):format( self.options.role ) )
    end
    
    local fail_list = {}
    for check_name, check in pairs( self.role_checks ) do
        self:print_debug( ( "  check %s" ):format( tostring( check_name ) ) )
        if not self:perform_check( check ) then
            table.insert( fail_list, tostring( check_name ) )
        end
    end

    if self.options.print_checks then
        if #fail_list > 0 then
            self:print_warning( ( "You fail at %s" ):format( self:format_array( fail_list ) ) )
        else
            self:print( "You are awesome" )
        end
    end
    
    -- And now update the display

    local new_fail = #fail_list > 0 and #self.fail_list == 0
    local no_more_fail = #fail_list == 0 and #self.fail_list > 0
    self.fail_list = fail_list
    
    self:update_display()
end

function Banfail:perform_check( check )
    local pass_count = 0
    local fail_count = 0
    
    for i, v in ipairs( check ) do
        --self:print_debug( ( "%s: %s" ):format( tostring( i ), self:format_table( v, true ) ) )
        local test_name = v[ 1 ]
        local function_name = "check_if_" .. test_name

        local passed = true
        if self[ function_name ] and ( type( self[ function_name ] ) == "function" ) then
            passed = self[ function_name ]( self, v )
        else
            self:print_warning( ( "Unable to perform test %s, no such test implemented yet. Possibly a typo?" ):format( tostring( test_name ) ) )
            passed = true
        end
        
        if ( passed ) then
            pass_count = pass_count + 1
        else
            fail_count = fail_count + 1
        end
    end
    
    if ( fail_count == 0 ) then
        return true
    end

    if ( check.match_all ) then
        return fail_count == 0
    else
        return pass_count > 0
    end
end

function Banfail:check_if_spec( args )
    local expected_spec = args[ 2 ]

    local current_spec = GetActiveTalentGroup()

    self:print_debug( ( "    check_if_spec: expected %d, current %d" ):format( expected_spec, current_spec ) )
    return expected_spec == current_spec
end

function Banfail:check_if_equipped( args )
    local slot_name = args[ 2 ]
    local expected_item = args[ 3 ]

    local slot_id, _ = GetInventorySlotInfo( slot_name )
    if slot_id == nil then
        self:print_warning( ( "Unknown slot name %s in 'equipped' test" ):format( slot_name ) )
        return false
    end
    local item_link = GetInventoryItemLink( "player", slot_id )
    local item_id = "nothing"
    if item_link then
        item_id = self:link_to_id( item_link )
    end

    self:print_debug( ( "    check_if_equipped: expected %s, current %s" ):format( expected_item, item_id ) )
    return expected_item == item_id
end

function Banfail:check_if_enchant( args )
    local slot_name = args[ 2 ]
    local expected_enchant = tostring( args[ 3 ] )

    local slot_id, _ = GetInventorySlotInfo( slot_name )
    if slot_id == nil then
        self:print_warning( ( "Unknown equipped slot name %s in 'equipped' test" ):format( slot_name ) )
        return false
    end
    local item_link = GetInventoryItemLink( "player", slot_id )

    local item_id
    if item_link then
        item_id = self:link_to_id( item_link )
    end

    local enchant_id = "nothing"
    if item_id then
        enchant_id = item_id:match( "^item:%d+:(%d+):" )
    end

    self:print_debug( ( "    check_if_enchant: expected %s, current %s" ):format( expected_enchant, enchant_id ) )
    return expected_enchant == enchant_id
end

function Banfail:check_if_glyph( args )
    local expected_glyph = tostring( args[ 2 ] )
    
    -- FIXME: suboptimal to make print_debug easier ; rewrite to if ( debug ) else?

    local glyph_list = {}
    local glyph_count = GetNumGlyphSockets()
    for i = 1, glyph_count do
        local enabled, _, glyph_id, _ = GetGlyphSocketInfo( i )
        if enabled then
            table.insert( glyph_list, glyph_id )
        end
    end
    
    self:print_debug( ( "    check_if_glyph: expected %s, found these %s" ):format( expected_glyph, self:format_array( glyph_list ) ) )
    for i, v in ipairs( glyph_list ) do
        if tostring( v ) == expected_glyph then
            return true
        end
    end
    return false
end

function Banfail:check_if_stance( args )
    local expected_stance = tostring( args[ 2 ] )

    local stance_index = tonumber( GetShapeshiftForm() )
    local stance_name
    if stance_index > 0 then
        local _, name, _, _ = GetShapeshiftFormInfo( stance_index )
        stance_name = name
    end
    if not stance_name then
        stance_name = "Caster"
    end
    
    self:print_debug( ( "    check_if_stance: expected %s, found %s" ):format( expected_stance, stance_name ) )
    return expected_stance == stance_name
end

function Banfail:check_if_ilvl( args )
    local slot_names = args[ 2 ]
    local minimum_level = args[ 3 ]
    
    if slot_names == nil then
        slot_names = {
            "HeadSlot",
            "NeckSlot",
            "ShoulderSlot",
            "BackSlot",
            "ChestSlot",
            "WristSlot",
            "HandsSlot",
            "WaistSlot",
            "LegsSlot",
            "FeetSlot",
            "Finger0Slot",
            "Finger1Slot",
            "Trinket0Slot",
            "Trinket1Slot",
            "MainHandSlot",
            "SecondaryHandSlot",
            "RangedSlot",
        }
    elseif type( slot_names ) ~= "table" then
        slot_names = { slot_names }
    end

    local result = true
    for i, slot_name in ipairs( slot_names ) do
        local slot_id, _ = GetInventorySlotInfo( slot_name )
        if slot_id == nil then
            self:print_warning( ( "Unknown slot name %s in 'ilvl' test, ignoring" ):format( slot_name ) )
        else
            local item_link = GetInventoryItemLink( "player", slot_id )
            if item_link then
                local iname, _, _, ilvl = GetItemInfo( item_link )
                if ilvl < minimum_level then
                    result = false
                    if self.options.print_checks then
                        self:print_warning( ( "  item %s in slot %s has ilvl %d" ):format( iname, slot_name, ilvl ) )
                    end
                end
            end
        end
    end

    return result
end

function Banfail:check_if_itemcount( args )
    local item_name = tostring( args[ 2 ] )
    local minimum_count = tonumber( args[ 3 ] )

    local count = tonumber( GetItemCount( item_name ) )
    self:print_debug( ( "    check_if_itemcount %s: expected at least %d, found %d" ):format( item_name, minimum_count, count ) )
    return count >= minimum_count
end

function Banfail:link_to_id( link )
    return link:match( "^|c%x+|H(.+)|h%[.*%]" )
end

-- EOF --
